package fr.cephb.berkeleydb.dom;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;


import org.w3c.dom.DOMException;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;
import org.xml.sax.helpers.DefaultHandler;

import com.sleepycat.bind.tuple.LongBinding;
import com.sleepycat.bind.tuple.TupleBinding;
import com.sleepycat.bind.tuple.TupleInput;
import com.sleepycat.bind.tuple.TupleOutput;
import com.sleepycat.je.DatabaseConfig;
import com.sleepycat.je.DatabaseException;
import com.sleepycat.je.Environment;
import com.sleepycat.je.EnvironmentConfig;
import com.sleepycat.je.Transaction;

import fr.cephb.berkeleydb.db.PrimaryDB;
import fr.cephb.io.IOUtils;

/**
 * A temporary XML document stored on BerkeleyDB
 * @author pierre
 *
 */
public class Document
	extends Node
	{
	private Environment env;
	private long id_generator=0L;
	private File envDirectory;
	private PrimaryDB<Long,Node> id2node;
	
	private class SAXHandler
		extends DefaultHandler
		{
		private StringBuilder content=null;
		private Node root;
		private Node curr;
		
		SAXHandler()
			{
			}
		
		@Override
		public void startElement(String uri, String localName, String qName,
				Attributes attributes) throws SAXException
			{
			QName name=null;
			Element e=createElement(name);
			for(int i=0;i< attributes.getLength();++i)
				{
				e.setAttribute(name, attributes.getValue(i));
				}
			if(this.root==null)
				{
				this.root=e;
				this.curr=e;
				}
			else
				{
				this.curr.appendChild(e);
				this.curr=e;
				}
			}
		
		@Override
		public void endElement(String uri, String localName, String qName)
				throws SAXException
			{
			this.curr=this.curr.getParentNode();
			this.content.setLength(0);
			}
		
		
		@Override
		public void characters(char[] ch, int start, int length)
				throws SAXException
			{
			if(this.curr==null) return;
			this.curr.appendChild(createText(new String(ch,start,length)));
			}
		}
	
	/**
	 * Tuple binding for XML nodes
	 */
	private static class NodeBinding
		extends TupleBinding<Node>
		{
		private Document dom;
		NodeBinding(Document dom)
			{
			this.dom=dom;
			}
		
		private QName readQName(TupleInput in)
			{
			return new QName(in.readString(), in.readString(), in.readString());
			}
		
		private void writeQName(QName name,TupleOutput out)
			{
			out.writeString(name.getPrefix());
			out.writeString(name.getNamespaceURI());
			out.writeString(name.getLocalPart());
			}
		
		@Override
		public Node entryToObject(TupleInput in)
			{
			long id=in.readLong();
			short type=in.readShort();
			
			long parent_id=in.readLong();
			long prev_sib=in.readLong();
			long next_sib=in.readLong();
			Node node=null;
			switch(type)
				{
				case ELEMENT_NODE:
					long first_child=in.readLong();
					QName name= readQName(in);
					int nAtt= in.readInt();
					List<Attr> atts=new ArrayList<Attr>(nAtt);
					for(int i=0;i< nAtt;++i)
						{
						QName attN=readQName(in);
						String attV= in.readString();
						Attr attr= new Attr(attN, attV);
						atts.add(attr);
						}
					node= new Element(id,this.dom,name);
					node.first_child_id=first_child;
					break;
				case TEXT_NODE:
					String content= in.readString();
					node= new Text(id,this.dom,content);
					break;
				case COMMENT_NODE:
					String comm= in.readString();
					node= new Comment(id,this.dom,comm);
					break;
				case CDATA_SECTION_NODE:
					String cdata= in.readString();
					node= new CDataSection(id,this.dom,cdata);
					break;
				case DOCUMENT_NODE:
					throw new IllegalStateException("never happen");
				}
			node.id=id;
			node.parent_id=parent_id;
			node.prev_sibling_id=prev_sib;
			node.next_sibling_id=next_sib;
			return node;
			}
		@Override
		public void objectToEntry(Node node, TupleOutput out)
			{
			out.writeLong(node.id);
			out.writeShort(node.getNodeType());
			out.writeLong(node.parent_id);
			out.writeLong(node.prev_sibling_id);
			out.writeLong(node.next_sibling_id);
			switch(node.getNodeType())
				{
				case ELEMENT_NODE:
					out.writeLong(node.first_child_id);
					Element e=(Element)node;
					writeQName(e.getQName(),out);
					Attr atts[]=e.getAttributes();
					out.writeInt(atts.length);
					for(Attr att:atts)
						{
						writeQName(att.getQName(),out);
						out.writeString(att.getValue());
						}
					break;
				case TEXT_NODE:
				case CDATA_SECTION_NODE:
				case COMMENT_NODE:
					String s=((AbstractDataNode)node).getText();
					out.writeString(s);
					break;
				case DOCUMENT_NODE:
					break;
				}
			}
		}
	
	Document()
		{	
		super(-1L,null);
		}
	
	@Override
	public Document getOwnerDocument()
		{
		return this;
		}
	
	@Override
	protected PrimaryDB<Long,Node> getDatabase()
		{
		return this.id2node;
		}
	
	void synch(Node n) throws DatabaseException
		{
		if(n.getOwnerDocument()!=this) throw new DOMException(DOMException.WRONG_DOCUMENT_ERR, "Bad document");
		getDatabase().put(getTransaction(), n.id, n);
		}
	
	@Override
	public int getNodeType()
		{
		return DOCUMENT_NODE;
		}
	
	public Element createElement(QName qName)
		{
		return new Element(this.id_generator++, this, qName);
		}
	
	public Text createText(String s)
		{
		return new Text(this.id_generator++,this,s);
		}
	
	public Comment createComment(String s)
		{
		return new Comment(this.id_generator++,this,s);
		}
	
	public CDataSection createCDataNode(String s)
		{
		return new CDataSection(this.id_generator++,this,s);
		}
	
	public Node getNodeById(long id)
		throws DatabaseException
		{
		return getDatabase().get(getTransaction(), id);
		}
	
	
	
	static Document newDocument()
		throws IOException
		{
		Transaction txn=null;
		Document dom=new Document();
		EnvironmentConfig envConfig= new EnvironmentConfig();
		envConfig.setAllowCreate(true);
		envConfig.setReadOnly(false);
		envConfig.setTransactional(false);
		
		dom.envDirectory= IOUtils.createTempDir("_xml", ".dom");
		dom.env= new Environment(dom.envDirectory, envConfig);
		
		DatabaseConfig dbConfig=new DatabaseConfig();
		dbConfig.setAllowCreate(true);
		dbConfig.setReadOnly(false);
		dbConfig.setTransactional(false);
		dom.id2node=new PrimaryDB<Long, Node>(dom.env, txn, dom.envDirectory.getName(),
					dbConfig,
					new LongBinding(),
					new NodeBinding(dom)
					);
		
		return dom;
		}
	
	public Node importNode(InputSource is)  throws SAXException,IOException
		{
		try
			{
			SAXParserFactory f=SAXParserFactory.newInstance();
			f.setNamespaceAware(true);
			f.setValidating(false);
			SAXParser parser= f.newSAXParser();
			SAXHandler h=new SAXHandler();
			parser.parse(is, h);
			return h.root;
			}
		catch (ParserConfigurationException e)
			{
			throw new SAXException(e);
			}
		
		}
	
	public void runSaxHandler(ContentHandler handler) throws SAXException,IOException
		{
		handler.startDocument();
		runSaxHandler(handler,getFirstChild());
		handler.endDocument();
		}
	
	private void runSaxHandler(ContentHandler handler,Node root) throws SAXException,IOException
		{
		if(root==null) return;
		switch(root.getNodeType())
			{
			case TEXT_NODE:
				{
				char ch[]= Text.class.cast(root).getText().toCharArray();
				handler.characters(ch, 0, ch.length);
				break;
				}
			case ELEMENT_NODE:
				{
				Element e=Element.class.cast(root);
				AttributesImpl atts=new AttributesImpl();
				for(Attr a:e.getAttributes())
					{
					atts.addAttribute(a.getNamespaceURI(), a.getLocalName(), a.getNodeName(), null, a.getValue());
					}
				handler.startElement(
						e.getNamespaceURI(),
						e.getLocalName(),
						e.getNodeName(),
						atts
						);
				Node curr=e.getFirstChild();
				while(curr!=null)
					{
					runSaxHandler(handler,curr);
					curr=curr.getNextSibling();
					}
				handler.endElement(null, null, null);
				break;
				}
			}
		}
	
	public void close()
		{
		if(this.id2node!=null)
			{
			this.id2node.close();
			}
		if(this.env!=null)
			{
			this.env.close();
			}
		for(File f: this.envDirectory.listFiles())
			{
			f.delete();
			}
		this.envDirectory.delete();
		}
	
	@Override
	public void write(XMLStreamWriter writer) throws XMLStreamException
		{
		Node first=getFirstChild();
		writer.writeStartDocument();
		if(first!=null) first.write(writer);
		writer.writeEndDocument();
		}
	
	@Override
	public String toString()
		{
		return "<?xml version='1.0' ?>";
		}
	
	public static void main(String[] args)
		{
		Document dom=null;
		try
			{
			dom=newDocument();
			Element e=dom.createElement(new QName(null, null, "Hello"));
			dom.appendChild(e);
			System.err.println("OK");
			}
		catch (Exception e)
			{
			e.printStackTrace();
			}
		finally
			{
			if(dom!=null) dom.close();
			}
		}
	
	}
